/*
 * Unidata Platform
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *
 * Commercial License
 * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *
 * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 * For clarification or additional options, please contact: info@unidata-platform.com
 * -------
 * Disclaimer:
 * -------
 * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 */
package org.unidata.mdm.dq.data.service.impl.function.data;

import java.util.Collections;
import java.util.Objects;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.data.Attribute;
import org.unidata.mdm.core.type.data.CodeAttribute;
import org.unidata.mdm.core.type.data.SimpleAttribute;
import org.unidata.mdm.core.type.data.SimpleAttribute.SimpleDataType;
import org.unidata.mdm.core.type.data.impl.AbstractSimpleAttribute;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.core.type.model.EntityElement;
import org.unidata.mdm.core.util.AttributeUtils;
import org.unidata.mdm.dq.core.context.CleanseFunctionContext;
import org.unidata.mdm.dq.core.dto.CleanseFunctionResult;
import org.unidata.mdm.dq.core.exception.CleanseFunctionExecutionException;
import org.unidata.mdm.dq.core.service.impl.function.system.AbstractSystemCleanseFunction;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionConfiguration;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionExecutionScope;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionInputParam;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionOutputParam;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionPortFilteringMode;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionPortInputType;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionPortValueType;
import org.unidata.mdm.dq.core.type.constant.CleanseConstants;
import org.unidata.mdm.dq.core.type.io.DataQualityError;
import org.unidata.mdm.dq.core.type.rule.SeverityIndicator;
import org.unidata.mdm.dq.core.util.DQUtils;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.type.search.EntityIndexType;
import org.unidata.mdm.search.context.SearchRequestContext;
import org.unidata.mdm.search.dto.SearchResultDTO;
import org.unidata.mdm.search.dto.SearchResultHitDTO;
import org.unidata.mdm.search.dto.SearchResultHitFieldDTO;
import org.unidata.mdm.search.service.SearchService;
import org.unidata.mdm.search.type.FieldType;
import org.unidata.mdm.search.type.form.FieldsGroup;
import org.unidata.mdm.search.type.form.FormField;
import org.unidata.mdm.search.type.query.SearchQuery;
import org.unidata.mdm.search.type.sort.SortField;
import org.unidata.mdm.system.util.TextUtils;

/**
 * Fetch data from inner entities
 */
public class InnerFetch extends AbstractSystemCleanseFunction {
    /**
     * Display name code.
     */
    private static final String FUNCTION_DISPLAY_NAME = "app.dq.functions.data.inner.fetch.display.name";
    /**
     * Description code.
     */
    private static final String FUNCTION_DESCRIPTION = "app.dq.functions.data.avoid.duplicates.decsription";
    /**
     * IP1.
     */
    private static final String INPUT_ENTITY_NAME_PORT = "entity-name";
    /**
     * IP1 name code.
     */
    private static final String INPUT_ENTITY_NAME_NAME = "app.dq.functions.data.inner.fetch.input.entity-name.name";
    /**
     * IP1 description code.
     */
    private static final String INPUT_ENTITY_NAME_DESCRIPTION = "app.dq.functions.data.inner.fetch.input.entity-name.decsription";
    /**
     * IP2.
     */
    private static final String INPUT_RETURN_NAME_PORT = "return-field-name";
    /**
     * IP2 name code.
     */
    private static final String INPUT_RETURN_NAME_NAME = "app.dq.functions.data.inner.fetch.input.return-field-name.name";
    /**
     * IP2 description code.
     */
    private static final String INPUT_RETURN_NAME_DESCRIPTION = "app.dq.functions.data.inner.fetch.input.return-field-name.decsription";
    /**
     * IP3.
     */
    private static final String INPUT_SEARCH_NAME_PORT = "search-field-name";
    /**
     * IP3 name code.
     */
    private static final String INPUT_SEARCH_NAME_NAME = "app.dq.functions.data.inner.fetch.input.search-field-name.name";
    /**
     * IP3 description code.
     */
    private static final String INPUT_SEARCH_NAME_DESCRIPTION = "app.dq.functions.data.inner.fetch.input.search-field-name.decsription";
    /**
     * IP4.
     */
    private static final String INPUT_SEARCH_VALUE_PORT = "search-value";
    /**
     * IP4 name code.
     */
    private static final String INPUT_SEARCH_VALUE_NAME = "app.dq.functions.data.inner.fetch.input.search-value.name";
    /**
     * IP4 description code.
     */
    private static final String INPUT_SEARCH_VALUE_DESCRIPTION = "app.dq.functions.data.inner.fetch.input.search-value.decsription";
    /**
     * IP5.
     */
    private static final String INPUT_ORDER_NAME_PORT = "order-field-name";
    /**
     * IP5 name code.
     */
    private static final String INPUT_ORDER_NAME_NAME = "app.dq.functions.data.inner.fetch.input.order-field-name.name";
    /**
     * IP5 description code.
     */
    private static final String INPUT_ORDER_NAME_DESCRIPTION = "app.dq.functions.data.inner.fetch.input.order-field-name.decsription";
    /**
     * IP6.
     */
    private static final String INPUT_FETCH_MODE_PORT = "fetch-mode";
    /**
     * IP6 name code.
     */
    private static final String INPUT_FETCH_MODE_NAME = "app.dq.functions.data.inner.fetch.input.fetch-mode.name";
    /**
     * IP6 description code.
     */
    private static final String INPUT_FETCH_MODE_DESCRIPTION = "app.dq.functions.data.inner.fetch.input.fetch-mode.decsription";
    /**
     * OP1 name code.
     */
    private static final String OUTPUT_PORT_1_NAME = "app.dq.functions.data.inner.fetch.output.port1.name";
    /**
     * OP1 description code.
     */
    private static final String OUTPUT_PORT_1_DESCRIPTION = "app.dq.functions.data.inner.fetch.output.port1.decsription";
    /**
     * OP2 name code.
     */
    private static final String OUTPUT_PORT_2_NAME = "app.dq.functions.data.inner.fetch.output.port2.name";
    /**
     * OP2 description code.
     */
    private static final String OUTPUT_PORT_2_DESCRIPTION = "app.dq.functions.data.inner.fetch.output.port2.decsription";
    /**
     * OP3 name code.
     */
    private static final String OUTPUT_PORT_3_NAME = "app.dq.functions.data.inner.fetch.output.port3.name";
    /**
     * OP3 description code.
     */
    private static final String OUTPUT_PORT_3_DESCRIPTION = "app.dq.functions.data.inner.fetch.output.port3.decsription";
    /**
     * This function configuration.
     */
    private static final CleanseFunctionConfiguration CONFIGURATION
        = CleanseFunctionConfiguration.configuration()
            .supports(CleanseFunctionExecutionScope.GLOBAL, CleanseFunctionExecutionScope.LOCAL)
            .input(CleanseFunctionConfiguration.port()
                    .name(INPUT_ENTITY_NAME_PORT)
                    .displayName(() -> TextUtils.getText(INPUT_ENTITY_NAME_NAME))
                    .description(() -> TextUtils.getText(INPUT_ENTITY_NAME_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE)
                    .valueTypes(CleanseFunctionPortValueType.STRING)
                    .required(true)
                    .build())
            .input(CleanseFunctionConfiguration.port()
                    .name(INPUT_RETURN_NAME_PORT)
                    .displayName(() -> TextUtils.getText(INPUT_RETURN_NAME_NAME))
                    .description(() -> TextUtils.getText(INPUT_RETURN_NAME_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE)
                    .valueTypes(CleanseFunctionPortValueType.STRING)
                    .required(true)
                    .build())
            .input(CleanseFunctionConfiguration.port()
                    .name(INPUT_SEARCH_NAME_PORT)
                    .displayName(() -> TextUtils.getText(INPUT_SEARCH_NAME_NAME))
                    .description(() -> TextUtils.getText(INPUT_SEARCH_NAME_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE)
                    .valueTypes(CleanseFunctionPortValueType.STRING)
                    .required(true)
                    .build())
            .input(CleanseFunctionConfiguration.port()
                    .name(INPUT_SEARCH_VALUE_PORT)
                    .displayName(() -> TextUtils.getText(INPUT_SEARCH_VALUE_NAME))
                    .description(() -> TextUtils.getText(INPUT_SEARCH_VALUE_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE, CleanseFunctionPortInputType.CODE)
                    .valueTypes(CleanseFunctionPortValueType.ANY)
                    .required(false)
                    .build())
            .input(CleanseFunctionConfiguration.port()
                    .name(INPUT_ORDER_NAME_PORT)
                    .displayName(() -> TextUtils.getText(INPUT_ORDER_NAME_NAME))
                    .description(() -> TextUtils.getText(INPUT_ORDER_NAME_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE)
                    .valueTypes(CleanseFunctionPortValueType.STRING)
                    .required(true)
                    .build())
            .input(CleanseFunctionConfiguration.port()
                    .name(INPUT_FETCH_MODE_PORT)
                    .displayName(() -> TextUtils.getText(INPUT_FETCH_MODE_NAME))
                    .description(() -> TextUtils.getText(INPUT_FETCH_MODE_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE)
                    .valueTypes(CleanseFunctionPortValueType.INTEGER)
                    .required(true)
                    .build())
            .output(CleanseFunctionConfiguration.port()
                    .name(CleanseConstants.OUTPUT_PORT_1)
                    .displayName(() -> TextUtils.getText(OUTPUT_PORT_1_NAME))
                    .description(() -> TextUtils.getText(OUTPUT_PORT_1_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE)
                    .valueTypes(CleanseFunctionPortValueType.ANY)
                    .required(false)
                    .build())
            .output(CleanseFunctionConfiguration.port()
                    .name(CleanseConstants.OUTPUT_PORT_2)
                    .displayName(() -> TextUtils.getText(OUTPUT_PORT_2_NAME))
                    .description(() -> TextUtils.getText(OUTPUT_PORT_2_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE)
                    .valueTypes(CleanseFunctionPortValueType.BOOLEAN)
                    .required(false)
                    .build())
            .output(CleanseFunctionConfiguration.port()
                    .name(CleanseConstants.OUTPUT_PORT_3)
                    .displayName(() -> TextUtils.getText(OUTPUT_PORT_3_NAME))
                    .description(() -> TextUtils.getText(OUTPUT_PORT_3_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE)
                    .valueTypes(CleanseFunctionPortValueType.INTEGER)
                    .required(false)
                    .build())
            .build();
    /**
     * Index of the first column
     */
    private static final int FIRST_HIT = 0;

    /**
     * Single result count
     */
    private static final long SINGLE_RESULT_COUNT = 1;

    /**
     * Search service.
     */
    @Autowired
    private SearchService searchService;
    /**
     * Meta Model service
     */
    @Autowired
    private MetaModelService metaModelService;
    /**
     * Instantiates a new cleanse function abstract.
     */
    public InnerFetch() {
        super("InnerFetch", () -> TextUtils.getText(FUNCTION_DISPLAY_NAME), () -> TextUtils.getText(FUNCTION_DESCRIPTION));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public CleanseFunctionConfiguration configure() {
        return CONFIGURATION;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public CleanseFunctionResult execute(CleanseFunctionContext ctx) {

        CleanseFunctionResult result = new CleanseFunctionResult();

        CleanseFunctionInputParam entityName = ctx.getInputParam(INPUT_ENTITY_NAME_PORT); // String
        CleanseFunctionInputParam returnFieldName = ctx.getInputParam(INPUT_RETURN_NAME_PORT); // String
        CleanseFunctionInputParam orderFieldName = ctx.getInputParam(INPUT_ORDER_NAME_PORT); // String
        CleanseFunctionInputParam searchFieldName = ctx.getInputParam(INPUT_SEARCH_NAME_PORT); // String
        CleanseFunctionInputParam searchValue = ctx.getInputParam(INPUT_SEARCH_VALUE_PORT); // Attribute
        CleanseFunctionInputParam fetchMode = ctx.getInputParam(INPUT_FETCH_MODE_PORT); // Long

        if ((Objects.isNull(entityName) || entityName.isEmpty())
         || (Objects.isNull(returnFieldName) || returnFieldName.isEmpty())
         || (Objects.isNull(orderFieldName) || orderFieldName.isEmpty())
         || (Objects.isNull(searchFieldName) || searchFieldName.isEmpty())
         || (Objects.isNull(fetchMode) || fetchMode.isEmpty())) {

            result.addError(DataQualityError.builder()
                    .ruleName(ctx.getRuleName())
                    .functionName(getName())
                    .category(DQUtils.CATEGORY_SYSTEM)
                    .message(TextUtils.getText("app.dq.functions.data.inner.fetch.missing.parameters"))
                    .severity(SeverityIndicator.RED)
                    .build());

            return result;
        }

        EntityElement entity = metaModelService.instance(Descriptors.DATA)
                .getElement(entityName.toSingletonValue());

        if (entity == null) {
            throw new CleanseFunctionExecutionException(ctx.getFunctionName(),
                    TextUtils.getText("app.dq.functions.data.inner.fetch.entity.element.notExist", entityName.toSingletonValue()));
        }

        SearchRequestContext context = createSearchRequest(ctx, entity,
                searchValue != null && !searchValue.isEmpty() ? searchValue.getSingleton() : null,
                searchFieldName.toSingletonValue(),
                returnFieldName.toSingletonValue(),
                orderFieldName.toSingletonValue());

        SearchResultDTO search = searchService.search(context);

        Attribute fetchedObject = retrieveAttr(search, entity, returnFieldName.toSingletonValue(), fetchMode.toSingletonValue());

        long count = search.getTotalCount();
        boolean justOne = count == SINGLE_RESULT_COUNT;

        result.putOutputParam(CleanseFunctionOutputParam.of(CleanseConstants.OUTPUT_PORT_1, fetchedObject));
        result.putOutputParam(CleanseFunctionOutputParam.of(CleanseConstants.OUTPUT_PORT_2, justOne));
        result.putOutputParam(CleanseFunctionOutputParam.of(CleanseConstants.OUTPUT_PORT_3, count));

        return result;
    }

    @Nonnull
    private SearchRequestContext createSearchRequest(
            CleanseFunctionContext ctx,
            EntityElement entity,
            Attribute attr,
            String searchFieldName,
            String returnFieldName,
            String orderFieldName) {

        SimpleAttribute<?> searchValue = createSearchAttr(ctx, entity, attr, searchFieldName);

        FieldType simpleDataType = searchValue.getDataType().toSearchType();

        FieldsGroup group = FieldsGroup.and(
                FormField.exact(
                        simpleDataType, searchValue.getName(), simpleDataType == FieldType.STRING, searchValue.getValue()));

        SortField sortField = createSortField(entity, orderFieldName);

        return SearchRequestContext.builder(EntityIndexType.RECORD, entity.getName())
               .query(SearchQuery.formQuery(group))
               .totalCount(true)
               .count(Integer.MAX_VALUE)
               .sorting(Collections.singletonList(sortField))
               .page(0)
               .returnFields(Collections.singletonList(returnFieldName))
               .skipEtalonId(true)
               .build();
    }

    private SimpleAttribute<?> createSearchAttr(CleanseFunctionContext ctx, EntityElement entity, Attribute attr, String searchFieldName) {

        AttributeElement model = entity.getAttributes().get(searchFieldName);
        SimpleDataType dataType = SimpleDataType.fromModelType(model.getValueType());

        if (attr == null) {
            return AbstractSimpleAttribute.of(dataType, searchFieldName);
        }

        SimpleDataType valueDataType = null;
        Object value = null;
        switch (attr.getAttributeType()) {
        case SIMPLE:
            SimpleAttribute<?> simpleAttribute = (SimpleAttribute<?>) attr;
            value = simpleAttribute.getValue();
            valueDataType = simpleAttribute.getDataType();
            break;
        case CODE:
            CodeAttribute<?> codeAttribute = (CodeAttribute<?>) attr;
            value = codeAttribute.getValue();
            valueDataType = codeAttribute.getDataType() == CodeAttribute.CodeDataType.STRING
                    ? SimpleDataType.STRING
                    : SimpleDataType.INTEGER;
            break;
        default:
            throw new CleanseFunctionExecutionException(ctx.getFunctionName(),
                    TextUtils.getText("app.dq.functions.data.inner.fetch.unsupported.attribute.type",
                            attr.getAttributeType()));
        }

        if (dataType != valueDataType && dataType == SimpleDataType.STRING && value != null) {
            value = value.toString();
        }

        SimpleAttribute<?> searchValue = AbstractSimpleAttribute.of(valueDataType, searchFieldName);
        searchValue.castValue(value);

        return searchValue;
    }

    private SortField createSortField(EntityElement entity, String orderFieldName) {

        AttributeElement el = entity
                .getAttributes()
                .get(orderFieldName);

        return SortField.of(el.getIndexed(), SortField.SortOrder.ASC);
    }

    @Nullable
    private Attribute retrieveAttr(SearchResultDTO search, EntityElement entity, String returnFieldName, Long fetchMode) {

        AttributeElement attr = entity.getAttributes().get(returnFieldName);

        SimpleDataType dataType = SimpleDataType.fromModelType(attr.getValueType());
        SimpleAttribute<?> simpleAttribute = AbstractSimpleAttribute.of(dataType, CleanseConstants.OUTPUT_PORT_1);

        if (search.getTotalCount() == 0 || search.getHits().isEmpty()) {
            return simpleAttribute;
        }

        SearchResultHitDTO hit = fetchMode == 0
                ? search.getHits().get(FIRST_HIT)
                : search.getHits().get(search.getHits().size() - 1);

        SearchResultHitFieldDTO value = hit.getFieldValue(returnFieldName);
        if (value == null || value.getValues().isEmpty()) {
            return simpleAttribute;
        }

        AttributeUtils.processSimpleAttributeValue(simpleAttribute, value.getFirstValue());
        return simpleAttribute;
    }
}
